﻿//-----------------------------------------------------------------------
// <copyright file="ManipulatorBase.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Manipulator
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using TileTool.Common.Math;
    using TileTool.Editor.Tool.Scene;
    using TileTool.Input;
    using TileTool.Input.Focus;
    using TileTool.Input.Keyboard;
    using TileTool.Input.Mouse;
    using TileTool.Render;

    /// <summary>
    /// Base class for the visual manipulators that you interact with
    /// in the canvas.
    /// </summary>
    public abstract class ManipulatorBase
    {
        #region Fields - Private

        /// <summary>
        /// Manipulator's matrix.
        /// </summary>
        private Matrix2D matrix = Matrix2D.Identity;

        /// <summary>
        /// Manipulator's size.
        /// </summary>
        private Size2D size = new Size2D(0.0f);

        /// <summary>
        /// List of handles we are using.
        /// </summary>
        private IList<ManipulatorHandle> handleList = new List<ManipulatorHandle>();

        #endregion

        #region Constructor

        /// <summary>
        /// Default constructor.
        /// </summary>
        public ManipulatorBase()
        {
            this.ResetDragData();
        }

        #endregion

        #region Events - Public

        /// <summary>
        /// Event triggered when a manipulation of an axis begins.
        /// </summary>
        public event Action<ManipulateEventArgs> Begin;

        /// <summary>
        /// Event triggered when a manipulation of an axis ends.
        /// </summary>
        public event Action<ManipulateEventArgs> End;

        /// <summary>
        /// Event triggered when a manipulation of an axis needs an update.
        /// </summary>
        public event Action<ManipulateEventArgs> Update;

        /// <summary>
        /// Event triggered when a manipulation of an axis is aborted.
        /// </summary>
        public event Action<ManipulateEventArgs> Abort;

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets or sets the owner of the manipulator.
        /// 
        /// Owner will be set by the manipulator tool when it is given the manipulator.
        /// </summary>
        public ManipulatorToolBase Owner
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the matrix for the manipulator.
        /// 
        /// The matrix will be set by the manipulator tool which owns this manipulator.
        /// </summary>
        public Matrix2D Matrix
        {
            get
            {
                return this.matrix;
            }

            set
            {
                this.matrix = value;
                this.MatrixChanged();
            }
        }

        /// <summary>
        /// Gets or sets the matrix of the manipulator when the drag started.
        /// </summary>
        public Matrix2D DragStartMatrix
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the size of the manipulator. The size is the size that bounds
        /// all currently selected objects.
        /// </summary>
        public Size2D Size
        {
            get
            {
                return this.size;
            }

            set
            {
                this.size = value;
                this.SizeChanged();
            }
        }

        /// <summary>
        /// Gets or sets the size of the manipulator when the drag started.
        /// </summary>
        public Size2D DragStartSize
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets the amount that has been dragged in world space.
        /// </summary>
        public Vector2D DragAmountWorld
        {
            get
            {
                return this.DragCurrentWorldPos - this.DragStartWorldPos;
            }
        }

        #endregion

        #region Properties - Protected

        /// <summary>
        /// Gets an enumerable of the handles we have.
        /// </summary>
        protected IEnumerable<ManipulatorHandle> Handles
        {
            get
            {
                return this.handleList; 
            }
        }

        /// <summary>
        /// Gets the handle that the mouse is currently hovering over.
        /// </summary>
        protected ManipulatorHandle HoverHandle
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the handle that the mouse is currently dragging.
        /// </summary>
        protected ManipulatorHandle DragHandle
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets if the manipulator is being dragged.
        /// </summary>
        protected bool Dragging
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the drag start position in world space.
        /// </summary>
        protected Vector2D DragStartWorldPos
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the drag current drag position in world space.
        /// </summary>
        protected Vector2D DragCurrentWorldPos
        {
            get;
            set;
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Reset the manipulator back to a default state.
        /// </summary>
        public virtual void Reset()
        {
        }

        /// <summary>
        /// Called to draw the manipulator.
        /// 
        /// The draw call will be made after the manipulator's matrix has been pushed on
        /// to the matrix stack.
        /// </summary>
        /// <param name="renderData">Render data for manipulator drawing.</param>
        public virtual void Draw(IRenderData renderData)
        {
            ICanvasRenderer renderer = renderData.Renderer;

            renderer.PushMatrix(this.Matrix);
            
            this.DrawManipulatorHandles(renderData);

            renderer.PopMatrix();
        }

        /// <summary>
        /// Handle input data from a canvas.
        /// </summary>
        /// <param name="sceneInputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        public virtual InputHandled HandleInput(SceneToolInputData sceneInputData)
        {
            InputData inputData = sceneInputData.InputData;

            switch (inputData.InputType)
            {
                case InputDataType.MouseMove:
                    return this.HandleMouseMove(sceneInputData, (MouseMoveInputData)inputData);
                case InputDataType.MouseButton:
                    return this.HandleMouseButton(sceneInputData, (MouseButtonInputData)inputData);
                case InputDataType.Key:
                    return this.HandleKey(sceneInputData, (KeyInputData)inputData);
                case InputDataType.Focus:
                    return this.HandleFocus(sceneInputData, (FocusInputData)inputData);
            }

            return InputHandled.No;
        }

        /// <summary>
        /// Force an abort if we are manipulating.
        /// </summary>
        public void ForceAbort()
        {
            this.DragAbort();
        }

        #endregion

        #region Methods - Protected

        /// <summary>
        /// Called when the manipulator's matrix changes.
        /// </summary>
        protected virtual void MatrixChanged()
        {
        }

        /// <summary>
        /// Called when the manipulator's size changes.
        /// </summary>
        protected virtual void SizeChanged()
        {
        }

        #endregion

        #region Methods - Protected - Handle functions

        /// <summary>
        /// Add a handle to the list of handles we are managing.
        /// </summary>
        /// <param name="handle">Manipulator handle to add.</param>
        protected virtual void AddManipulatorHandle(ManipulatorHandle handle)
        {
            if (handle == null)
            {
                throw new ArgumentNullException("handle", "Handle must be non-null.");
            }

            this.handleList.Add(handle);
        }

        /// <summary>
        /// Draw all manipulator handles.
        /// </summary>
        /// <param name="renderData">Render data for manipulator drawing.</param>
        protected virtual void DrawManipulatorHandles(IRenderData renderData)
        {
            // We draw the handles in reverse order so that handles added first are
            // drawn on top of all others.
            int count = this.handleList.Count;
            for (int i = count - 1; i >= 0; --i)
            {
                ManipulatorHandle handle = this.handleList[i];
                if (!handle.Visible)
                {
                    continue;
                }

                handle.Draw(renderData);
            }
        }

        /// <summary>
        /// Clear the selected state from all handles.
        /// </summary>
        protected void ClearManipulatorHandleSelection()
        {
            this.HoverHandle = null;
            this.DragHandle = null;

            foreach (ManipulatorHandle handle in this.handleList)
            {
                handle.Selected = false;
            }
        }

        /// <summary>
        /// Handle mouse move for our manipulator handles. Will only be called when we are not in a drag.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the mouse move was handled.</returns>
        protected virtual InputHandled HandleMouseMoveForManipulatorHandles(SceneToolInputData sceneInputData, MouseMoveInputData inputData)
        {
            // First pass clear all selected state.
            this.ClearManipulatorHandleSelection();
            
            Vector2D worldPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, inputData.Pos);
            Matrix2D invMatrix = this.Matrix.Invert();
            Vector2D localPos = invMatrix.Multiply(worldPos);

            foreach (ManipulatorHandle handle in this.handleList)
            {
                Vector2D handleLocal = localPos - handle.Pos;
                if (handle.TestPoint(handleLocal))
                {
                    handle.Selected = true;
                    this.HoverHandle = handle;
                    return InputHandled.Yes;
                }
            }

            return InputHandled.No;
        }

        #endregion

        #region Methods - Protected - Drag functions

        /// <summary>
        /// Called when a drag begins.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the drag started.</param>
        protected virtual void DragBegin(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
            Debug.Assert(this.HoverHandle != null, "Hover handle must not be null at this point.");

            Vector2D worldPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, canvasPos);

            this.Dragging = true;
            this.DragHandle = this.HoverHandle;
            this.DragStartMatrix = this.Matrix;
            this.DragStartSize = this.Size;
            this.DragStartWorldPos = worldPos;
            this.DragCurrentWorldPos = worldPos;

            foreach (ManipulatorHandle handle in this.handleList)
            {
                handle.DragStartPos = handle.Pos;
            }

            if (this.DragHandle.TriggerManipulate)
            {
                Action<ManipulateEventArgs> temp = this.Begin;
                if (temp != null)
                {
                    temp(new ManipulateEventArgs(this, sceneInputData));
                }
            }
        }

        /// <summary>
        /// Called when a drag ends successfully.
        /// 
        /// Drags which end unsuccessfully will call AbortDrag().
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <returns>Returns true if we actually dragged and modified objects.</returns>
        protected virtual bool DragEnd(SceneToolInputData sceneInputData)
        {
            if (this.DragHandle.TriggerManipulate)
            {
                Action<ManipulateEventArgs> temp = this.End;
                if (temp != null)
                {
                    temp(new ManipulateEventArgs(this, sceneInputData));
                }
            }

            bool res = true;
            Vector2D dragAmount = this.DragAmountWorld;
            if (dragAmount.x == 0 && dragAmount.y == 0)
            {
                res = false;
            }

            // Call this after the event so that we can use functions
            // which check for Dragging during the event.
            this.ResetDragData();

            return res;
        }

        /// <summary>
        /// Called when a drag update happens.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the mouse is currently.</param>
        protected virtual void DragUpdate(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
            Vector2D worldPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, canvasPos);

            this.DragCurrentWorldPos = worldPos;

            if (this.DragHandle.TriggerManipulate)
            {
                Action<ManipulateEventArgs> temp = this.Update;
                if (temp != null)
                {
                    temp(new ManipulateEventArgs(this, sceneInputData));
                }
            }
        }

        /// <summary>
        /// Called when a drag needs to be aborted.
        /// </summary>
        protected virtual void DragAbort()
        {
            if (!this.Dragging)
            {
                return;
            }

            bool triggerManipulate = this.DragHandle.TriggerManipulate;

            this.ResetDragData();

            if (triggerManipulate)
            {
                Action<ManipulateEventArgs> temp = this.Abort;
                if (temp != null)
                {
                    temp(new ManipulateEventArgs(this, null));
                }
            }
        }

        #endregion

        #region Methods - Protected - Input functions

        /// <summary>
        /// Handle mouse move input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleMouseMove(SceneToolInputData sceneInputData, MouseMoveInputData inputData)
        {
            if (this.Dragging)
            {
                this.DragUpdate(sceneInputData, inputData.Pos);
                return InputHandled.Yes;
            }

            return this.HandleMouseMoveForManipulatorHandles(sceneInputData, inputData);
        }

        /// <summary>
        /// Handle mouse button input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleMouseButton(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            if (inputData.Down)
            {
                switch (inputData.Button)
                {
                    case MouseButton.Left:
                        return this.HandleLeftMouseButtonDown(sceneInputData, inputData);
                    case MouseButton.Right:
                        return this.HandleRightMouseButtonDown(sceneInputData, inputData);
                    case MouseButton.Middle:
                        return this.HandleMiddleMouseButtonDown(sceneInputData, inputData);
                }
            }
            else
            {
                switch (inputData.Button)
                {
                    case MouseButton.Left:
                        return this.HandleLeftMouseButtonUp(sceneInputData, inputData);
                    case MouseButton.Right:
                        return this.HandleRightMouseButtonUp(sceneInputData, inputData);
                    case MouseButton.Middle:
                        return this.HandleMiddleMouseButtonUp(sceneInputData, inputData);
                }
            }

            return InputHandled.No;
        }

        /// <summary>
        /// Handle left mouse button down.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleLeftMouseButtonDown(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            if (this.HoverHandle == null)
            {
                return InputHandled.No;
            }

            this.DragBegin(sceneInputData, inputData.Pos);

            return InputHandled.Yes;
        }

        /// <summary>
        /// Handle left mouse button up.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleLeftMouseButtonUp(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            if (!this.Dragging)
            {
                return InputHandled.No;
            }

            if (this.DragEnd(sceneInputData))
            {
                return InputHandled.Yes;
            }

            return InputHandled.No;
        }

        /// <summary>
        /// Handle right mouse button down.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleRightMouseButtonDown(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle right mouse button up.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleRightMouseButtonUp(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle middle mouse button down.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleMiddleMouseButtonDown(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle middle mouse button up.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleMiddleMouseButtonUp(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle key input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleKey(SceneToolInputData sceneInputData, KeyInputData inputData)
        {
            if (inputData.Down)
            {
                return this.HandleKeyDown(sceneInputData, inputData);
            }
            else
            {
                return this.HandleKeyUp(sceneInputData, inputData);
            }
        }

        /// <summary>
        /// Handle key down input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleKeyDown(SceneToolInputData sceneInputData, KeyInputData inputData)
        {
            if (inputData.Key != InputKey.Escape)
            {
                return InputHandled.No;
            }

            this.DragAbort();

            return InputHandled.Yes;
        }

        /// <summary>
        /// Handle key up input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleKeyUp(SceneToolInputData sceneInputData, KeyInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle focus input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleFocus(SceneToolInputData sceneInputData, FocusInputData inputData)
        {
            if (inputData.Focus)
            {
                return this.HandleGainedFocus(sceneInputData, inputData);
            }
            else
            {
                return this.HandleLostFocus(sceneInputData, inputData);
            }
        }

        /// <summary>
        /// Handle gained focus input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleGainedFocus(SceneToolInputData sceneInputData, FocusInputData inputData)
        {
            return InputHandled.No;
        }

        /// <summary>
        /// Handle lost focus input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected virtual InputHandled HandleLostFocus(SceneToolInputData sceneInputData, FocusInputData inputData)
        {
            this.DragAbort();

            return InputHandled.No;
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Reset all drag related data.
        /// </summary>
        private void ResetDragData()
        {
            this.HoverHandle = null;
            this.DragHandle = null;
            this.Dragging = false;
            this.DragStartWorldPos = Vector2D.Zero;
            this.DragCurrentWorldPos = Vector2D.Zero;
            this.DragStartMatrix = Matrix2D.Identity;
            this.DragStartSize = new Size2D(0.0f);

            this.ClearManipulatorHandleSelection();
        }

        #endregion
    }
}
